前言
这篇文章是@gtg师傅写的,征得同意分享给了我们。(l0你在装什么你根本看不懂啊喂(#`O′)!)
是好东西,我一个二进制菜狗虽然看不明白但是想在博客挂一份,或许以后会有学
这篇文章所有权以及一切解释归@gtg所有,如需转载请联系@gtg。
原型链prototype chain
没有类的实例对象
Javascript继承机制的设计思想): Brendan Eich设计 Javasciprt 的初衷只是为了浏览器与网页互动,因而主要目标就是高效。
当时的热门编程语言Java
和C++
都使用new
命令生成实例,如果要一个 class 要继承另一个,就直接用相应的语法继承就行了。
BE在 Javascript 中学习了new
的命令方法,但考虑到JS的简洁易用性,他不想要引入"类"的概念,转而使用"类的构造函数"(constructor)替代。
比如创建对象的时候,C++的写法是:
1 | ClassName *object = new ClassName(param); |
Java的写法是:
1 | Foo foo = new Foo(); |
而Javascript:
1 | function DOG(name){ |
可以看到 JS 中并没有声明类( Java 和 C++ 中笔者没写出来),而是在创建实例的时候直接执行定义的DOG()
构造函数。这样看来语法就变得简洁很多了。
原型链解决共享属性问题
上文用构造函数创建的每一个实例对象,都有自己的属性和方法的副本,不受其他对象的属性更改影响。这样不仅无法做到数据共享,也是极大的资源浪费。
考虑到这一点,BE决定为构造函数设定一个prototype
属性。
这个prototype
属性包含一个对象,所有需要共享的属性都放在这个对象中,不需要共享的属性则各自通过构造函数定义或直接定义。
比如:
1 | function DOG(name){ |
这样一来就可以轻易地修改来自同一"构造函数"构造的实例对象的属性了。
原型访问过程
既然prototype
是赋予构造函数的原型,那在对象中是如何访问原型属性的呢?
这是通过对象的__proto__
属性实现的。比如上面的 dogA 对象,他的__proto__
指向DOG.prototype
。当你访问dogA.species
时,发现dogA
并不存在species
属性,这时就会访问dogA.__proto__.species
,也就是DOG.prototype.species
,发现这个属性是存在的,所以就返回了原型的属性值"犬科"。当然如果存在多级原型关系的话,同样会去寻找__proto__.__proto__.__proto__...
。这样的靠__proto__
串起来的逐级访问过程就是原型链。
而实际上,在JS中任何一个对象都具有__proto__
属性,这一属性指向的是底层对象Object.prototype
。当构造函数的prototype
被指向其他对象时,你仍然可以通过dogA.__proto__.__proto__
去访问底层Object.prototype
,这也是原型链污染经常利用的点。
方法的原型关系与属性相同,例子如下:
1 | function Person(name, age) { |
这种设计使得 Javescript 语法简洁了许多,既满足了共享属性的需求,又可以通过实例的各自定义满足独立属性或方法的需求。
需要提及的是赋值操作不会自动去访问原型,而是创建该实例的独立属性或方法,不会自动覆盖原型的属性或方法
插入一张图片做为总结
更多细节参见:該來理解 JavaScript 的原型鍊了 - Huli
注:
class
关键字以及一系列与类相关的方法在后来的ES6标准
中出现,但仍然保留了原型关系特性(有些人认为这个类只是原型链的语法糖
作用域与闭包
作用域Scope
作用域就是一个变量的有效范围。哪里呼叫这个参数能指向他的存储地址的,都是其作用域。
在 Javascript 中,每一个 function
进入之前都会有一个 Execution Contexts
执行环境,存放了执行需要的所有信息。这个 Execution Contexts
又带有一个与之对应的 variable object
,在这个对象中存放需要的所有变量。
如果在某一个 function
中呼叫 a
变量,解释器就会从最小的、即自身的 variable object
中寻找 a
变量,然后逐级向上访问,直到访问到 a
的值,或访问到 Global Scope
没有发现而抛出错误为止。这种从 Global Scope
到一层层 fuction
就构成了整体的 scope chain
。
举个例子
1 | var a = 1 |
在这样一段代码中, echo()
函数执行之前似乎又有了 a
变量的重新定义。但实际上在 js 作用域只讲层级不讲先后, test()
中定义的 a
并不会对 echo()
的执行环境造成任何影响。所以这里的输出结果是 1。
这种特性叫做lexical scope
静态作用域,相对应的自然还有 dynamic scope
动态作用域
比如下面这两段
1 | var b = 2; |
JS真费脑筋。。动态作用域具体可以参考这篇 你不知道的Javascript动态作用域 - 掘金 (juejin.cn)
大概就是使用特殊的方法eval
和with
强行修改作用域,造成"欺骗"。
闭包Closure
先看这段代码
1 | function test() { |
将内部函数inner()
作为返回值传出赋值给外部函数,执行后发现仍能获取test()
内部定义的a
变量。
这意味着原有test()
函数在执行后没有回收而是继续占用内存。只要函数inner()
存在,test()
函数相关的资源(包括test和a的定义)都不会被释放。这种看似执行完毕却还有东西没有释放的,就叫做Closure
闭包。
那么闭包的优点是什么?
1 | function getWallet() { |
闭包这种写法能保证内部变量my_balance
仅允许限定的方法进行修改,并且由于内存始终没有释放,内部变量也可以记录状态。
闭包还能够解决下面这样的问题
1 | var btn = document.querySelectorAll('button') //获取所有button |
代码原意是想让每一个按钮弹出不同的数字1,2,3,4,但实际上只会弹出5。这是因为监听器内的函数仅仅是得到了定义而没有执行。而对参数i
的调用无法再内部的作用域找到,只能在全局作用域中找到一个已经经历循环的i
,因而每一个按钮都会弹出5。
如果采用闭包的方法改成下面这样
1 | function getAlert(num) { |
循环的过程中调用 getAlert()
返回的function
不再依附于i
,生成了独立的五个function
就能实现预期的功能。还可以使用Immediately-Invoked Function Expression
即调函数表达式的方法,这里就不再记录了。
但实际上这种写法还是有点麻烦,幸好ES6
加入了一个叫做block scope
块级作用域的东西。
块级作用域就是仅在一定范围内的生效的作用域,外部无法访问。function
的{}
包裹内容实际上也是块
,但一般不称作块级作用域
。
仅仅像下面这样将var
修改为let
就能解决上面的问题
1 | for(let i=0; i<=4; i++) { |
let
的声明方法会为每一个function
创造一个针对他的块级作用域,使得上述代码可以理解为这样
1 | { |
(看起来并不是很高效
function的建立和执行过程
1 | 一、建立全局执行环境,获取其中被定义的所有对象,进行声明(没有赋值),并记录其中哪个是函数 |
如果說你認為閉包一定要:「離開創造它的環境」,那顯然「所有的函式都是閉包」這句話就不成立;但如果你認同閉包的定義是:「由函式和與其相關的參照環境組合而成的實體」,那就代表在 JavaScript 裡面,所有的函式都是閉包。
嘶~~
本来还打算做一点参数传递、变量提升的内容总结,但是我能力有限删删改改千来字,发现总结不了((🌿
参见https://blog.huli.tw/recommend/JavaScript 五講,非常精彩,文章大部分的参考也来自这里。
如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !